package com.kj.misc.service;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.digitalpetri.modbus.master.ModbusTcpMaster;
import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
import com.digitalpetri.modbus.requests.ReadHoldingRegistersRequest;
import com.digitalpetri.modbus.requests.ReadInputRegistersRequest;
import com.digitalpetri.modbus.requests.WriteSingleRegisterRequest;
import com.digitalpetri.modbus.responses.ReadHoldingRegistersResponse;
import com.digitalpetri.modbus.responses.ReadInputRegistersResponse;
import com.digitalpetri.modbus.responses.WriteSingleRegisterResponse;
import com.google.common.collect.Maps;
import com.ivan.toolkit.common.Sleeps;
import com.ivan.toolkit.common.WatchDog;
import com.kj.misc.domain.MiscFetcherConfig;
import com.kj.misc.domain.MiscFetcherConfig.FetcherEntry;
import com.kj.misc.domain.MiscFetcherConfig.ParameterManager;
import com.kj.misc.domain.MiscFetcherConfig.ValueEntry;

import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

@Log4j2
public class MiscFetcher implements Runnable {
	
	public static final String SampleFetcherName = "SAMPLE";
	
	@Getter
	private FetcherEntry context = null;
	
	ScheduledExecutorService scheduler = null;
	
	private MiscFetcherManager manager = null;
	
	private Map<String, FetcherHandler> handlers = Maps.newLinkedHashMap();
	
	@Getter
	private WatchDog watchdog = new WatchDog(10000);
	
	@Setter @Getter
	private boolean enabled = true;
	
	private long counterFetch = 0;
	private long counterFetchSuccess = 0;
	private long counterFetchFailed = 0;
	
	public MiscFetcher(MiscFetcherManager manager, FetcherEntry fe) {
		this.manager = manager;
		this.context = fe;
		
		long live = Math.max(10, (fe.getInterval() + fe.getTimeout()) * 2 + 5);
		watchdog.setLivemillis(live);
	}
	
	public String formatState() {
		StringBuilder sb = new StringBuilder()
				.append(context.getName())
				.append(" : [")
				.append("enabled=").append(this.isEnabled())
				.append(",state=").append(watchdog.isAlive())
				.append(",total=").append(this.counterFetch)
				.append(", success=").append(this.counterFetchSuccess)
				.append(", failed=").append(this.counterFetchFailed)
				.append("]");
		return sb.toString();
	}

	public void start() {

		scheduler = Executors.newScheduledThreadPool(1);
		
		scheduler.scheduleWithFixedDelay(
				this, 5000, 
				context.getInterval(), 
				TimeUnit.MILLISECONDS);
		
		log.info("MiscFecther started.");
	}

	public void stop() {

		try {
			scheduler.shutdown();
			scheduler.awaitTermination(3, TimeUnit.SECONDS);
			scheduler = null;
		} catch (InterruptedException e) {
		} finally {
			log.info("MiscFecther exit.");
		}
	}
	
	public boolean started() {
		return scheduler != null;
	}
	
	@Override
	public void run() {
		
		if(!this.isEnabled()) {
			log.info("Fetcher " + context.getName() + " is disabled");
			return;
		}
		
		if(context.getName().startsWith(SampleFetcherName)) {
			
			watchdog.keepAlive();
			
			try {
				// do fetch
				List<ValueEntry> entries = context.getValues();
				for(ValueEntry ve : entries) {
					Optional<Double> op = SAMPLE_FetcherHandler.handle(null, context, ve);
					if(op.isPresent()) {
						manager.dispatch(MiscFetcherConfig.key(context, ve), op.get());
					}
				}
			} catch(Exception e) {
				log.error("Fetch sample error.", e);
			}
			

						
		} else {
			
			ModbusTcpMaster master = null;
									
			try {
				
				// build master
				master = buildMaster();

				// do fetch	
				List<ValueEntry> entries = context.getValues();
				for(ValueEntry ve : entries) {
					fetch(master, context, ve);
					Sleeps.sleepQuietly(200);
				}
				
				this.counterFetchSuccess++;
				
				// keep state
				watchdog.keepAlive();

				
			} catch(Exception e) {

				this.counterFetchFailed++;
				
			} finally {
				
				// close the master
				try {
					if(master != null)
						master.disconnect().get(1000, TimeUnit.MILLISECONDS);
				} catch (Exception e) {
				} finally {
					master = null;
				}
				
				this.counterFetch++;
				
			}

		}

	}
	
	private ModbusTcpMaster buildMaster() throws Exception {
		
		ModbusTcpMasterConfig mtmc = 
				new ModbusTcpMasterConfig.Builder(context.getHost())
				.setPort(context.getPort())
				.setTimeout(Duration.ofMillis(context.getTimeout()))
				.build();
			
		try {
			ModbusTcpMaster master = new ModbusTcpMaster(mtmc);
			return master.connect().get(context.getTimeout(), TimeUnit.MILLISECONDS);
		} catch(Exception e) {
			//log.error("Connect to device failed.", e);
			throw e;
		}
		
	}
	
	private void fetch(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) throws Exception {
		
		String type = ve.getValueType();
		
		FetcherHandler handler = getHandler(type);
		if(handler == null) {
			log.warn("FetcherHandler not found by name: " + type);
			throw new Exception("FetcherHandler not found by name: " + type);
		}
		
		Optional<Double> op = handler.handle(master, fe, ve);
		if(op.isPresent()) {
			manager.dispatch(MiscFetcherConfig.key(fe, ve), op.get());
		}
	}
	
	/**03*/
	private static ByteBuf readHoldingRegisters(ModbusTcpMaster master, int slave, int address, int quantity) {
		
		ReadHoldingRegistersRequest request = 
				new ReadHoldingRegistersRequest(address, quantity);
		
		try {
			CompletableFuture<ReadHoldingRegistersResponse> future = 
					master.sendRequest(request, slave);
			
			ReadHoldingRegistersResponse response = 
					future.get(1500, TimeUnit.MILLISECONDS);
			return response.getRegisters();
		} catch(Exception e) {
		}
		return null;
	}
	
	/**06*/
	private static boolean writeHoldingRegisters(ModbusTcpMaster master, int slave, int address, int value) {
		
		WriteSingleRegisterRequest request = 
				new WriteSingleRegisterRequest(address, value);
		
		try {
			CompletableFuture<WriteSingleRegisterResponse> future = 
					master.sendRequest(request, slave);
			
			future.get(1500, TimeUnit.MILLISECONDS);
			return true;
		} catch(Exception e) {
			return false;
		}
	}
	
	/**04*/
	private static ByteBuf readInputRegisters(ModbusTcpMaster master, int slave, int address, int quantity) {
		
		ReadInputRegistersRequest request = 
				new ReadInputRegistersRequest(address, quantity);
				
		try {
			CompletableFuture<ReadInputRegistersResponse> future = 
					master.sendRequest(request, slave);
			ReadInputRegistersResponse response = 
					future.get(1500, TimeUnit.MILLISECONDS);
			return response.getRegisters();
		} catch(Exception e) {
		}
		return null;
	}
	
	/*
	private static Map<String, FetcherHandler> handlers = Maps.newLinkedHashMap();
	
	static {
		
		handlers.put("DAM_E3070D", new DAM_E3070D_FetcherHandler());
		handlers.put("DAM_E3058F", new DAM_E3058F_FetcherHandler());
		handlers.put("ALS", new DAM_ALS36_FetcherHandler());
		handlers.put("COMMON_FLOAT", new COMMON_FLOAT_FetcherHandler());
		//handlers.put("SAMPLE", new SAMPLE_FetcherHandler());
	}
	*/
	
	private FetcherHandler getHandler(String key) {
		
		if(handlers.containsKey(key)) {
			return handlers.get(key);
		}
		
		FetcherHandler handler = this.buildHandler(key);
		if(handler == null) return null;
		
		handlers.put(key, handler);
		return handler;
		
	}
	
	private FetcherHandler buildHandler(String key) {
		switch(key) {
		case "DAM_E3070D": return new DAM_E3070D_FetcherHandler();
		case "DAM_E3058F": return new DAM_E3058F_FetcherHandler();
		case "ALS": return new DAM_ALS36_FetcherHandler();
		case "COMMON_FLOAT": return new COMMON_FLOAT_FetcherHandler();
		case "COMMON_DOUBLE": return new COMMON_DOUBLE_FetcherHandler();
		case "CN01": return new CN01_FetcherHandler();
		default: return null;
		}
	}
	
	
	public static interface FetcherHandler {
		
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve);
		
	}
	
	public static class DAM_E3070D_FetcherHandler implements FetcherHandler {

		protected void initalizeStart(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
			int channelIndex = pr.getOrDefault("channelIndex", 0);
			int startAddress = channelIndex == 1 ? 773 : 772;
						
			writeHoldingRegisters(master, slaveAddress, startAddress - 4, 0x03);
			writeHoldingRegisters(master, slaveAddress, startAddress, 0x02);
			
			log.debug("Start counter for DAM_3070D Module.");
			
		}
		
		protected Optional<Long> fetch(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
			int channelIndex = pr.getOrDefault("channelIndex", 0);
			//double valueRatio = pr.getOrDefault("valueRatio", 1.0D);
			
			int startAddress = channelIndex == 1 ? 794 : 792;
			
			ByteBuf bb = readHoldingRegisters(master, slaveAddress, startAddress, 2);
			if(bb != null && bb.isReadable(4)) {
				int low = bb.readUnsignedShort();
				int high = bb.readUnsignedShort();
				long value = (high << 16) + low;
				return Optional.of(value);
			}
			
			return Optional.empty();
			
		}

		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			initalizeStart(master, fe, ve);
			
			Optional<Long> op = fetch(master, fe, ve);
			if(op.isPresent()) {
				ParameterManager pr = ParameterManager.at(ve.getParameters());
				double valueRatio = pr.getOrDefault("valueRatio", 1.0D);
				return Optional.of(op.get() * valueRatio);
				
			} else {
				
			}
			
			return Optional.empty();
		}
		
	}
	
	public static class DAM_E3058F_FetcherHandler implements FetcherHandler {


        private static final double minValue = (double)0;
        
        private static final double maxValue = (double)0xFFFF;
		
		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
			int channelIndex = pr.getOrDefault("channelIndex", 0);
            channelIndex = Math.max(channelIndex, 0);
            channelIndex = Math.min(channelIndex, 7);
            
            double referMin = pr.getOrDefault("referMin", 4D);
            double referMax = pr.getOrDefault("referMax", 20D);
            
            int startAddress = 256 + channelIndex * 2;
            
			ByteBuf bb = readInputRegisters(master, slaveAddress, startAddress, 2);
			if(bb != null && bb.isReadable(4)) {
				
				int v1 = bb.readUnsignedShort();
				int v2 = bb.readUnsignedShort();

				long value = v1 + (v2 << 16);
				
				double v = Utilities.translateValueToReferValue((double)value, referMin, referMax, minValue, maxValue);
				
				return Optional.of(v);
			}
			
			return Optional.empty();
			
		}
		
	}
	
	public static class DAM_ALS36_FetcherHandler implements FetcherHandler {

		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
            int startAddress = pr.getOrDefault("startAddress", 1);
            
			ByteBuf bb = readHoldingRegisters(master, slaveAddress, startAddress, 4);
			if(bb != null && bb.isReadable(8)) {
				
				float v1 = bb.readFloat();
				float v2 = bb.readFloat();
				
				double value = v1 * 1000000 + v2;

				return Optional.of(value);
			}
			return Optional.empty();
		}
		
	}
	
	public static class COMMON_FLOAT_FetcherHandler implements FetcherHandler {

		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
            int startAddress = pr.getOrDefault("startAddress", 1);
            
            String cmd = pr.getOrDefault("cmd", "03");
            
            boolean swapped = pr.getOrDefault("swapped", "0").equals("1");
            
            if(cmd.equals("04") || cmd.equals("4") || cmd.equalsIgnoreCase("input")) {
            	
				ByteBuf bb = readInputRegisters(master, slaveAddress, startAddress, 2);
				if(bb != null && bb.isReadable(4)) {
					double value = swapped ? bb.readFloatLE() : bb.readFloat();
					return Optional.of(value);
				}
				
            } else {
            	
				ByteBuf bb = readHoldingRegisters(master, slaveAddress, startAddress, 2);
				if(bb != null && bb.isReadable(4)) {
					double value = swapped ? bb.readFloatLE() : bb.readFloat();
					return Optional.of(value);
				}
				
            }
			return Optional.empty();
		}
		
	}
	
	public static class COMMON_DOUBLE_FetcherHandler implements FetcherHandler {

		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
            int startAddress = pr.getOrDefault("startAddress", 1);
            
            String cmd = pr.getOrDefault("cmd", "03");
            
            boolean swapped = pr.getOrDefault("swapped", "0").equals("1");
            
            if(cmd.equals("04") || cmd.equals("4") || cmd.equalsIgnoreCase("input")) {
            	
				ByteBuf bb = readInputRegisters(master, slaveAddress, startAddress, 4);
				if(bb != null && bb.isReadable(8)) {
					double value = swapped ? bb.readDoubleLE() : bb.readDouble();
					return Optional.of(value);
				}
				
            } else {
            	
				ByteBuf bb = readHoldingRegisters(master, slaveAddress, startAddress, 4);
				if(bb != null && bb.isReadable(8)) {
					double value = swapped ? bb.readDoubleLE() : bb.readDouble();
					return Optional.of(value);
				}
				
            }
			return Optional.empty();
		}
		
	}
	
	/**
	 * 苍南流量计
	 * @author solod
	 *
	 */
	public static class CN01_FetcherHandler implements FetcherHandler {
		

		@Override
		public Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			int slaveAddress = pr.getOrDefault("slaveAddress", 1);
			slaveAddress = Math.max(slaveAddress, 1);
			slaveAddress = Math.min(slaveAddress, 255);
			
            int startAddress = pr.getOrDefault("startAddress", 1);
            
            int quantity = pr.getOrDefault("quantity", 4);
			
            int byteCount = quantity * 2;
            
			ByteBuf bb = readHoldingRegisters(master, slaveAddress, startAddress, quantity);
			if(bb != null && bb.isReadable(byteCount)) {
				
				if(byteCount == 8) {
					long val = bb.readLong();
					long v1 = val >> 16;
					double v2 = (double)(val & 0xFFFF) /  65536;
					return Optional.of(v1 + v2);
				} else if(byteCount == 4) {
					int val = bb.readInt();
					int v1 = val >> 16;
					double v2 = (double)(val & 0xFFFF) /  65536;
					return Optional.of(v1 + v2);
				} 				
			}
			
			return Optional.empty();
		}
		
	}
	
	public static class SAMPLE_FetcherHandler /*implements FetcherHandler*/ {
		
		private static Random random = new Random();

		//@Override
		public static Optional<Double> handle(ModbusTcpMaster master, FetcherEntry fe, ValueEntry ve) {
			
			ParameterManager pr = ParameterManager.at(ve.getParameters());
			
			double min = pr.getOrDefault("minValue", 0D);
			double max = pr.getOrDefault("maxValue", 10D);

			return Optional.of(min + random.nextDouble() * Math.abs(max - min));
		}
		
	}
	
	static class Utilities {
		
        public static double translateValueToReferValue(
                double value, 
                double referMin, 
                double referMax, 
                double valueMin, 
                double valueMax)
            {
                double ratio = (value - valueMin) / (valueMax - valueMin);
                return ((referMax - referMin) * ratio) + referMin;
            }
		
		
	}
	
}
